/*
 * Copyright (c) 2010 Vadim Zhukov <persgray@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


#include "common.h"

#include <sys/socket.h>
#include <sys/un.h>

/* ntohl/htons() definitions */
#include <netinet/in.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "syslogd.h"


static struct logbufferhead	*sl_construct_logbuf_list(const char *,
				    size_t *);
static int	sl_connect(struct sl_context *);
static void	sl_close_conn(struct sl_context *);
static int	sl_init_buffers(struct sl_context *ctx, struct sl_task *task);
static void	sl_task_restart(struct sl_task *);
static void	sl_free_task_resources(struct sl_task *);

static const char *cmdnames[] = {
	"UNKNOWN",
	"READ",
	"READ AND CLEAR",
	"CLEAR",
	"LIST",
	"FLAGS",
	"READ CONTINIOUSLY"
};

static const char *phasenames[] = {
	"START",
	"SEND_CMD",
	"RECV_RR",
	"RECV_DATA",
	"DONE"
};

/*
 * Initialized automatically if needed.
 *
 * XXX TODO: Detect path run-time, not simply use default.
 */
struct sockaddr_un	sl_sockpath;


int
sl_init_context(struct sl_context *ctx, struct logbufferhead *reqbuffers) {
	bzero(ctx, sizeof(struct sl_context));
	ctx->reqbuffers = reqbuffers;
	ctx->logsock = -1;
	SIMPLEQ_INIT(&ctx->tasks);
	ctx->addrlen = sizeof(struct sockaddr_un);
#ifndef __linux__
	sl_sockpath.sun_len = ctx->addrlen;
#endif
	sl_sockpath.sun_family = AF_UNIX;
	if (sl_sockpath.sun_path[0] == 0)
		strncpy(sl_sockpath.sun_path, DEF_SYSLOG_SOCK,
		    sizeof(sl_sockpath.sun_path));
	memcpy(&ctx->sun, &sl_sockpath, sizeof(struct sockaddr_un));
	return 0;
};

/*
 * Free all resources. You can reuse context later with sl_init_context().
 */
void
sl_deinit_context(struct sl_context *ctx) {
	struct	sl_task *task;

	while (!SIMPLEQ_EMPTY(&ctx->tasks)) {
		task = SIMPLEQ_FIRST(&ctx->tasks);
		task->phase = SL_TASK_PHASE_DONE;
		SIMPLEQ_REMOVE_HEAD(&ctx->tasks, entry);
		sl_free_task(task);
	}
	if (ctx->logsock != -1) {
		(void)close(ctx->logsock);
		ctx->logsock = -1;
	}
}

/*
 * Create list of log buffer names from the string formatted as
 * "name1 name2 name3 ". Do not use strtok(3)/strsep(3) as they modify data.
 */
struct logbufferhead *
sl_construct_logbuf_list(const char *s, size_t *pcount) {
	struct	logbufferhead *head;
	struct	logbuffer *lb, *lbprev;
	ssize_t	n, count;
	int	delim, errcode;
	const	char *e;

	head = malloc(sizeof(struct logbufferhead));
	if (head == NULL)
		return NULL;
	SLIST_INIT(head);

	delim = ' ';
	lbprev = NULL;
	count = 0;
	while (1) {
		lb = NULL;

		for (; *s != '\0'; s++)
			if (*s != ' ' && *s != '\n')
				break;
		if (*s == '\0')
			break;

		e = strchr(s, delim);
		if (e == NULL)
			e = s + strlen(s);
		n = e - s;
		if (n == 0) {
			DPRINTFX(1, "invalid log names list");
			errcode = EINVAL;
			goto err;
		} else if (n >= MAX_MEMBUF_NAME) {
			DPRINTFX(1, "log name too long");
			errcode = ENAMETOOLONG;
			goto err;
		}
		lb = malloc(sizeof(struct logbuffer));
		if (lb == NULL) {
			errcode = errno;
			DPRINTFX(1, "construct_logbuf_list: malloc");
			goto err;
		}
		bzero(lb, sizeof(struct logbuffer));
		/* careful here! */
		if (e[-1] == '\n')
			n--;
		if (n > 0 && e[-1] == '*') {
			lb->lb_overflowed = 1;
			n--;
		}
		if (n == 0) {
			DPRINTFX(1, "invalid log names list");
			errcode = EINVAL;
			goto err;
		}
		memcpy(lb->lb_name, s, n);
		lb->lb_name[n] = '\0';

		if (lbprev == NULL)
			SLIST_INSERT_HEAD(head, lb, lb_entry);
		else
			SLIST_INSERT_AFTER(lbprev, lb, lb_entry);
		lbprev = lb;
		s += n;
		while (*++s == ' ');
		count++;
	}
	if (pcount != NULL)
		*pcount = count;
	return head;

err:
	free(lb);
	while (!SLIST_EMPTY(head)) {
		lb = SLIST_FIRST(head);
		SLIST_REMOVE_HEAD(head, lb_entry);
		free(lb);
	}
	free(head);
	errno = errcode;
	return NULL;
}

/*
 * Errrm... Maybe macro will be better?
 */
int
sl_is_gone(struct sl_context *ctx) {
	return (ctx->logsock == -1);
}

/*
 * Checks if we're connected to syslogd, connect if not.
 *
 * After connection, read (new) list of available buffer names. If user
 * requested some then perform cross of those sets. Warn if user requested
 * unavailable buffer.
 */
int
sl_connect(struct sl_context *ctx) {
	int	 oerrno;

	ctx->logsock = socket(AF_UNIX, SOCK_STREAM, 0);
	if (ctx->logsock == -1)
		goto err;
	if (fcntl(ctx->logsock, F_SETFL, (int)O_NONBLOCK) == -1)
		goto err;
	if (connect(ctx->logsock, (struct sockaddr *)&ctx->sun,
	    ctx->addrlen) == -1)
		goto err;
	return 0;

err:
	oerrno = errno;
	DPRINTF(0, "sl_connect");
	sl_close_conn(ctx);
	errno = oerrno;
	return -1;
}

/*
 * This one will not ever fail, he-he.
 */
void
sl_close_conn(struct sl_context *ctx) {
	if (ctx->logsock != -1) {
		DPRINTFX(3, "sl_close_conn, closing logsock");
		(void)close(ctx->logsock);
		ctx->logsock = -1;
	}
}

/*
 * Initialize buffers list based on results of LIST command.
 */
int
sl_init_buffers(struct sl_context *ctx, struct sl_task *task) {
	struct	logbufferhead *head, *oldbuf;
	struct	logbuffer *lb, *lb2;
	size_t	nbuf;
	int	oerrno;

	head = NULL;

	if (sl_task_failed(task)) {
		errno = task->error;
		goto err;
	}
	if (sl_task_code(task) != SL_CMD_LIST) {
		errno = EINVAL;
		goto err;
	}

	head = sl_construct_logbuf_list(task->result, &nbuf);
	if (head == NULL)
		goto err;

	if (ctx->reqbuffers != NULL && !SLIST_EMPTY(ctx->reqbuffers)) {
		SLIST_FOREACH(lb, head, lb_entry)
			lb->lb_used = 0;
		SLIST_FOREACH(lb2, ctx->reqbuffers, lb_entry) {
			lb2->lb_found = 0;
			SLIST_FOREACH(lb, head, lb_entry)
				if (!strcmp(lb2->lb_name, lb->lb_name)) {
					lb->lb_used = 1;
					lb2->lb_found = 1;
					break;
				}
			if (!lb2->lb_found)
				DPRINTFX(1, "log buffer %s was not found",
				    lb2->lb_name);
		}
	} else
		SLIST_FOREACH(lb, head, lb_entry)
			lb->lb_used = 1;

	oldbuf = ctx->buffers;
	if (ctx->nbuffers > nbuf)
		/* prevent problems with races */
		ctx->nbuffers = nbuf;
	ctx->buffers = head;
	ctx->nbuffers = nbuf;
	if (oldbuf != NULL) {
		while (!SLIST_EMPTY(oldbuf)) {
			lb = SLIST_FIRST(oldbuf);
			SLIST_REMOVE_HEAD(oldbuf, lb_entry);
			free(lb);
		}
		free(oldbuf);
	}
	return 0;

err:
	oerrno = errno;
	DPRINTF(0, "sl_init_buffers");
	sl_free_task(task);
	if (head != NULL) {
		while (!SLIST_EMPTY(head)) {
			lb = SLIST_FIRST(head);
			SLIST_REMOVE_HEAD(head, lb_entry);
			free(lb);
		}
	}
	errno = oerrno;
	return -1;
}

int
sl_update_buffers_list(struct sl_context *sl) {
	return sl_add_task(sl, SL_CMD_LIST, NULL);
}

/*
 * Go through the tasks queue and make sure it's filled with non-completed
 * "read & clear" commands for each requested buffer.
 */
int
sl_read_and_clear(struct sl_context *sl) {
	struct	logbuffer *lb;
	struct	sl_task *task;
	int	x;

	DPRINTFX(5, "sl_read_and_clear(): buffers is %p", sl->buffers);
	if (sl->buffers == NULL)
		return 0;
	SLIST_FOREACH(lb, sl->buffers, lb_entry) {
		DPRINTFX(5, "sl_read_and_clear(): checking buffer %s",
		    lb->lb_name);
		if (!lb->lb_used)
			continue;
		SIMPLEQ_FOREACH(task, &sl->tasks, entry) {
			switch (sl_task_code(task)) {
			case SL_CMD_READ_CLEAR:
			case SL_CMD_READ:
			case SL_CMD_READ_CONT:
				if (task->phase != SL_TASK_PHASE_DONE &&
				    strcmp(lb->lb_name, task->cmd.logname) == 0)
					goto next_buffer;
			}
		}
		if (sl_add_task(sl, SL_CMD_READ_CLEAR, lb->lb_name))
			return -1;
next_buffer:
		x = 0;
	}
	return 0;
}

/*
 * Basically validate input, construct task object and add it to the end of
 * tasks queue.
 *
 * Returns 0 on success, otherwise returns -1 and place error code in "errno".
 */
int
sl_add_task(struct sl_context *ctx, int cmd, const char *logbufname) {
	struct	sl_task *task;

	if (ctx->qlength >= SL_MAX_QUEUE_LENGTH) {
		errno = ENOBUFS;
		return -1;
	}

	switch (cmd) {
	case SL_CMD_READ:
	case SL_CMD_READ_CLEAR:
	case SL_CMD_CLEAR:
	case SL_CMD_FLAGS:
	case SL_CMD_LIST:
		break;

	case SL_CMD_READ_CONT:
		/* Do we need it at all? */
		errno = ENOTSUP;
		return -1;

	default:
		errno = EINVAL;
		return -1;
	}

	task = malloc(sizeof(struct sl_task));
	if (task == NULL)
		return -1;
	bzero(task, sizeof(struct sl_task));
	task->cmd.version = htonl(SL_CTL_VERSION);
	task->cmd.code = htonl(cmd);
	if (cmd != SL_CMD_LIST)
		if (strlcpy(task->cmd.logname, logbufname, MAX_MEMBUF_NAME)
		    >= MAX_MEMBUF_NAME) {
			errno = ENAMETOOLONG;
			goto err;
		}
	SIMPLEQ_INSERT_TAIL(&ctx->tasks, task, entry);
	ctx->qlength++;
	DSLTASK(3, task, "in sl_add_task");
	return 0;

err:
	free(task);
	return -1;
}

/*
 * Proceed tasks queue one-by-one - syslogd does not support multiple requests
 * at the same time. Tasks are restarted.
 *
 * This function returns -1 on _any_ error. The caller should investigate tasks
 * queue on itself; permanently failed tasks are marked as done.
 */
int
sl_proceed_tasks(struct sl_context *ctx) {
	struct	 sl_task *task;
	ssize_t	 n;
	char	*p;

	/*
	 * We do not check for sl_is_gone() before switch() clause to set
	 * ctx->has_completed_tasks if there are any such.
	 */

	SIMPLEQ_FOREACH(task, &ctx->tasks, entry) {
		DSLTASK(3, task, "in sl_proceed_tasks");
		switch (task->phase) {
		case SL_TASK_PHASE_START:
			if (sl_connect(ctx) == -1)
				goto err;
			task->phase = SL_TASK_PHASE_SEND_CMD;
			/* FALLTHROUGH */

		case SL_TASK_PHASE_SEND_CMD:
			if (sl_is_gone(ctx)) {
				errno = EBADF;
				goto err;
			}
			task->error = 0;
			p = (char*)&task->cmd;
			n = write(ctx->logsock, p + task->phase_progress,
			    sizeof(struct sl_cmd) - task->phase_progress);
			if (n == -1) {
				if (errno != EAGAIN)
					goto err;
				return 0;
			}
			task->phase_progress += n;
			if (task->phase_progress < sizeof(struct sl_cmd))
				return 0;
			task->phase = SL_TASK_PHASE_RECV_RR;
			task->phase_progress = 0;
			/* FALLTHROUGH */

		case SL_TASK_PHASE_RECV_RR:
			if (sl_is_gone(ctx)) {
				errno = EBADF;
				goto err;
			}
			task->error = 0;
			p = (char*)&task->rr;
			n = read(ctx->logsock, p + task->phase_progress,
			    sizeof(struct sl_reply_hdr)-task->phase_progress);
			if (n == -1) {
				/* EINTR is possible and OK here */
				if (errno != EAGAIN && errno != EINTR)
					goto err;
				return 0;
			}
			if (n == 0)
				/*
				 * We should get -1 with EAGAIN if there is no
				 * data, so treat this case as EOF.
				 */
				goto err;
			task->phase_progress += n;
			if (task->phase_progress < sizeof(struct sl_reply_hdr))
				return 0;
			task->phase = SL_TASK_PHASE_RECV_DATA;
			task->phase_progress = 0;
			/* FALLTHROUGH */

		case SL_TASK_PHASE_RECV_DATA:
			if (sl_is_gone(ctx)) {
				errno = EBADF;
				goto err;
			}
			task->error = 0;
			if (sl_task_code(task) == SL_CMD_FLAGS) {
				task->phase = SL_TASK_PHASE_DONE;
				goto task_done;
			}
			if (task->result == NULL) {
				/* first try */
				task->result = malloc(MAX_MEMBUF);
				if (task->result == NULL)
					goto err;
			}

			n = read(ctx->logsock,
			    task->result + task->phase_progress,
			    MAX_MEMBUF - task->phase_progress);
			if (n == -1) {
				/* EINTR is possible and OK here */
				if (errno != EAGAIN && errno != EINTR)
					goto err;
				return 0;
			}
			if (n == 0) {
				/* Looks like an EOF, see above */
				task->phase = SL_TASK_PHASE_DONE;
				goto task_done;
			}
			task->phase_progress += n;
			if (task->phase_progress == MAX_MEMBUF &&
			    task->result[MAX_MEMBUF - 1] != '\n') {
				errno = ENOBUFS;
				goto err;
			}
			return 0;

		case SL_TASK_PHASE_DONE:
			ctx->has_completed_tasks = 1;
			continue;

		default:
			fatalx("Unknown syslog task phase code %d",
			    task->phase);
		}

err:
		task->error = errno;
		sl_close_conn(ctx);
		sl_task_restart(task);
		errno = task->error;
		return -1;

task_done:
		sl_close_conn(ctx);
		if (sl_task_code(task) == SL_CMD_LIST && task->error == 0)
			sl_init_buffers(ctx, task);
		ctx->has_completed_tasks = 1;
	}
	return 0;
}

/*
 * Checks if there are any finished tasks. If there are no tasks at all,
 * set errno to ENOENT. Otherwise, if none tasks completed, set errno to EBUSY.
 * Returns NULL for both cases.
 *
 * For completed task, return pointer to it. It should be freed with
 * sl_free_task() later.
 */
struct sl_task *
sl_pick_done_task(struct sl_context *ctx) {
	struct	sl_task *task;

	if (SIMPLEQ_EMPTY(&ctx->tasks)) {
		errno = ENOENT;
		return NULL;
	}
	task = SIMPLEQ_FIRST(&ctx->tasks);
	if (!sl_task_done(task)) {
		errno = EBUSY;
		return NULL;
	}
	DSLTASK(3, task, "in sl_pick_done_task");
	SIMPLEQ_REMOVE_HEAD(&ctx->tasks, entry);
	ctx->qlength--;
	return task;
}

/*
 * Free the resources allocated for task. Should be called on completed tasks,
 * returns -1 and sets errno to EINVAL otherwise. It's OK to give a NULL pointer
 * to task, in this case function immediately succeeds.
 */
int
sl_free_task(struct sl_task *task) {
	if (task == NULL)
		return 0;
	DSLTASK(3, task, "in sl_free_task");
	if (!sl_task_done(task)) {
		errno = EINVAL;
		return -1;
	}
	sl_free_task_resources(task);
	free(task);
	return 0;
}

/*
 * Actually free resources. Internal use only.
 */
void
sl_free_task_resources(struct sl_task *task) {
	free(task->result);
}

void
sl_task_restart(struct sl_task *task) {
	switch (task->phase) {
	case SL_TASK_PHASE_RECV_DATA:
		free(task->result);
		/* FALLTHROUGH */
	}
	task->result = NULL;
	task->phase_progress = 0;
	task->phase = SL_TASK_PHASE_START;
	/* do not touch "error" */
}

void
sl_task_dump(FILE *f, const struct sl_task *task) {
	fprintf(f, "SYSLOG TASK: phase %s (%zu bytes progress), error %d\n",
	    phasenames[task->phase],
	    task->phase_progress,
	    task->error);
	fprintf(f, "\tCMD: %s version %u\n",
	    cmdnames[sl_task_code(task)],
	    ntohl(task->cmd.version));
}
